import pandas as pd
import numpy as np
import glob
import matplotlib.pyplot as plt
import random
Os arquivos devem ser baixados da Justiça Eleitoral: https://dadosabertos.tse.jus.br/dataset/resultados-2022-arquivos-transmitidos-para-totalizacao
Executando o script logjez_process.py arquivos CSV serão gerados para cada estado dois arquivos, um com votos e outro com dados das urnas.
Vamos carregar a tabela de cidades. Só será utilizada para informar o nome da cidade de acordo com o código
municipios = pd.read_csv("./municipios.csv", encoding="iso-8859-1")
municipios.sample(10)
| municipio_cod | municipio | estado | |
|---|---|---|---|
| 1052 | 94854 | NAZÃÂRIO | GO |
| 3198 | 74047 | LINDOESTE | PR |
| 2440 | 4227 | BRASIL NOVO | PA |
| 4785 | 32514 | UMBAÃÂBA | SE |
| 2491 | 4731 | ITUPIRANGA | PA |
| 3624 | 58319 | DUAS BARRAS | RJ |
| 5073 | 65315 | IRAPUÃÂ | SP |
| 4648 | 82791 | QUILOMBO | SC |
| 3101 | 11614 | PIMENTEIRAS | PI |
| 3105 | 11690 | PORTO | PI |
Vamos carregar os dados das urnas
path = "./csv_gerados/??.urnas.csv"
filenames = glob.glob(path)
dfs = []
for filename in filenames:
dfs.append(pd.read_csv(filename, encoding = "ISO-8859-1"))
urnas = pd.concat(dfs, ignore_index=True).dropna()
urnas.sample(10)
| UF | municipio | zona | secao | qtdEleitoresAptos | qtdComparecimento | dataHoraAbertura | dataHoraEncerramento | idPleito | qtdEleitoresLibCodigo | qtdEleitoresCompBiometrico | modelo | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 370965 | SP | 62910 | 274 | 78 | 388 | 313 | 20221030T080001 | 20221030T170135 | 407 | 22 | 253 | UE2015 |
| 117892 | MA | 9210 | 3 | 472 | 312 | 260 | 20221030T080057 | 20221030T170234 | 407 | 8 | 260 | UE2020 |
| 264638 | PR | 75353 | 2 | 486 | 346 | 285 | 20221030T080001 | 20221030T170019 | 407 | 14 | 254 | UE2020 |
| 348663 | SC | 81795 | 76 | 677 | 388 | 323 | 20221030T080001 | 20221030T170027 | 407 | 5 | 302 | UE2020 |
| 437045 | SP | 64416 | 297 | 172 | 308 | 191 | 20221030T080001 | 20221030T170140 | 407 | 18 | 173 | UE2020 |
| 347405 | SC | 83496 | 54 | 162 | 376 | 296 | 20221030T080001 | 20221030T170352 | 407 | 55 | 237 | UE2010 |
| 33156 | BA | 39497 | 116 | 46 | 288 | 214 | 20221030T080141 | 20221030T170025 | 407 | 4 | 184 | UE2020 |
| 339857 | RS | 85073 | 5 | 105 | 241 | 198 | 20221030T080001 | 20221030T170512 | 407 | 17 | 152 | UE2013 |
| 18811 | BA | 39551 | 69 | 26 | 246 | 179 | 20221030T080001 | 20221030T170004 | 407 | 10 | 76 | UE2020 |
| 79754 | DF | 97012 | 10 | 270 | 365 | 307 | 20221030T080001 | 20221030T170450 | 407 | 13 | 280 | UE2020 |
urnas.shape[0]
471984
Aqui carregamos os arquivo com os votos.
path = "./csv_gerados/??.votos.csv"
filenames = glob.glob(path)
dfs = []
for filename in filenames:
dfs.append(pd.read_csv(filename, encoding = "ISO-8859-1"))
votos = pd.concat(dfs, ignore_index=True).dropna()
votos.sample(10)
| UF | municipio | zona | secao | cargo | quantidadeVotos | partido | candidato | |
|---|---|---|---|---|---|---|---|---|
| 1522174 | SP | 67792 | 80 | 56 | presidente | 9 | nulo | nulo |
| 969158 | PR | 74730 | 28 | 221 | presidente | 4 | branco | branco |
| 1078950 | RJ | 60011 | 246 | 136 | presidente | 211 | 22 | 22 |
| 33993 | AL | 28690 | 17 | 64 | presidente | 120 | 13 | 13 |
| 28303 | AL | 28339 | 13 | 31 | presidente | 77 | 22 | 22 |
| 407716 | GO | 93343 | 28 | 125 | presidente | 149 | 22 | 22 |
| 1258937 | RS | 89354 | 110 | 71 | presidente | 89 | 13 | 13 |
| 454913 | MA | 7935 | 15 | 342 | presidente | 1 | nulo | nulo |
| 802805 | PB | 19810 | 17 | 261 | presidente | 9 | nulo | nulo |
| 552577 | MG | 43559 | 300 | 48 | presidente | 1 | branco | branco |
votos.shape[0]
1851420
Precisamos uma tabela com 2 colunas com totais de votos para os candidatos Bolsonaro e Lula
votosBolsonaro = votos.query("candidato == '22'")
votosLula = votos.query("candidato == '13'")
votosBolsonaro = votosBolsonaro.rename(columns={"quantidadeVotos":"Bolsonaro"})
votosLula = votosLula.rename(columns={"quantidadeVotos":"Lula"})
Aqui juntamos os dados com votos dos 2 candidatos e o modelo de urna
votosPresidente = votosBolsonaro[['UF','municipio','zona','secao','Bolsonaro']].join(
votosLula[['UF','municipio','zona','secao','Lula']]
.set_index(['UF','municipio','zona','secao']), how="outer",
on=['UF','municipio','zona','secao']).join(
urnas[['UF','municipio','zona','secao','modelo']]
.set_index(['UF','municipio','zona','secao']),
on=['UF','municipio','zona','secao']
)
votosPresidente.head(10)
| UF | municipio | zona | secao | Bolsonaro | Lula | modelo | |
|---|---|---|---|---|---|---|---|
| 1 | AC | 1015 | 2 | 88 | 84.0 | 53.0 | UE2009 |
| 5 | AC | 1015 | 2 | 75 | 129.0 | 47.0 | UE2009 |
| 7 | AC | 1015 | 2 | 69 | 125.0 | 53.0 | UE2009 |
| 11 | AC | 1015 | 2 | 95 | 133.0 | 34.0 | UE2009 |
| 14 | AC | 1015 | 2 | 90 | 144.0 | 47.0 | UE2009 |
| 18 | AC | 1015 | 2 | 73 | 138.0 | 45.0 | UE2009 |
| 22 | AC | 1015 | 2 | 74 | 129.0 | 48.0 | UE2009 |
| 24 | AC | 1015 | 2 | 67 | 164.0 | 53.0 | UE2009 |
| 28 | AC | 1015 | 2 | 80 | 124.0 | 98.0 | UE2009 |
| 32 | AC | 1015 | 2 | 66 | 162.0 | 56.0 | UE2009 |
votosPresidente.shape
(471525, 7)
O foco aqui é validar a suspeita de 1 modelo das urnas ter comportamento na totalização de votos, portanto devemos começar analizando como foram distribuidas as urnas, pelo menos por estado, para diminuir a probabiliade de o fator demografico determinar a diferença.
urnasPorUF = pd.pivot_table(data=votosPresidente[['UF','modelo']], index=['UF'], columns=['modelo'], aggfunc=np.count_nonzero)
ax = urnasPorUF.plot.bar(stacked=True, figsize=(8,6))
ax.set_title('Tipo de Urna Por Estado', fontsize=20)
Text(0.5, 1.0, 'Tipo de Urna Por Estado')
Acima vemos a distribuição. Notem que haviam urnas que não encontramos o arquivo de log (SemLog).
O gráfico mostra que as urnas foram distribuidas em todos os estados. Em especial o modelo mais novo que representa uma boa quantidade das urnas para cada estado (em rosa)
Os graficos abaixo mostram a distribuição de votos apurados nas urnas UE2020 e o contraste para as não UE2020 (anteriores a 2020 que não foram auditadas no pleito de 2022)
data = votosPresidente.query("modelo == 'UE2020'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
plt.title("Urnas UE2020 - Brasil")
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
data = votosPresidente.query("modelo != 'UE2020'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
plt.title("Urnas Não UE2020 - Brasil")
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
C:\Users\coelh\AppData\Local\Temp\ipykernel_26828\2442136202.py:2: UserWarning: No data for colormapping provided via 'c'. Parameters 'vmax' will be ignored plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
Os gráficos já mostram acima que o comportamento é muito distinto. As urnas mais antigas deram mais vantagem ao cadidato Lula, aparentemente retirando votos do Bolsonaro.
No gráfico das 2020 aparece um grande numero de votos mais ao centro, enquanto as antigas parecem ter algum tipo de trava, o que gera uma linha geometrica, quase igual um triângulo escaleno.
Vamos ver os mesmos gráficos para cada tipo de urna.
* note que existem 3 populações maiores de urnas, as UE2010, UE2015 e UE2020. Os demais modelos são minoria, o que pode dificultar essa análise de um ponto de vista geometrico
for index, row in votosPresidente.filter(['modelo'], axis=1).drop_duplicates().sort_values("modelo").iterrows():
data = votosPresidente.query("modelo == '"+row['modelo']+"'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
plt.title("Votos Brasil com urna modelo "+row["modelo"])
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
Revise os gráficos acima e note como é possível notar o tal triângulo escaleno em todos modelos, menos no modelo UE2020 onde, apesar de visualizarmos o triângulo, há um escape (massa da votos) mais distribuida ao centro.
Isso pode indicar algumas possiveis causas
Porém o mais provável é que a dispersão seja por conta de urnas com mais eleitores (vamos analisar abaixo), o que seria natural
Aqui vamos explorar as urnas UE2020 e não UE2020 por cada estado
for index, row in votosPresidente.filter(['UF'], axis=1).drop_duplicates().iterrows():
print(row['UF'])
data = votosPresidente.query("modelo == 'UE2020' and UF == '"+row['UF']+"'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
plt.title("Urnas UE2020 - UF: "+row['UF'])
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
data = votosPresidente.query("modelo != 'UE2020' and UF == '"+row['UF']+"'")
plt.scatter(data['Bolsonaro'],data['Lula'], s=1)
plt.title("Urnas Não UE2020 - UF: "+row['UF'])
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
AC
AL
AM
AP
BA
CE
DF
ES
GO
MA
MG
MS
MT
PA
PB
PE
PI
PR
RJ
RN
RO
RR
RS
SC
SE
SP
TO
ZZ
Você pode notar nos gráficos acima o achatamento, que fica sempre mais evidente nos estados onde o PT e Lula são mais populares. Nesses estados o achatamento do triângulo fica muito mais evidente.
Isso demonstra que, se há algorítimo, esse deve ter alguma lógica, não só aleatória, mas também inteligênte para realizar a migração de votos (fraude) levando em consideração a quantidade de votos de cada candidato adversário do número 13 (número do PT)
É muito provável que esse algoritmo estaria presente desde quando as urnas começaram a operar, o que pode ter favorecido o candidato a presidente do PT desde então.
A fraude só poderia então ser detectada porque as UE2020 não apresentaram o mesmo comportamento, ou o algoritmo falhou nessas urnas, criando a oportunidade de comparar os dados, ou não há fraude, e isso seria explicado somente pela existência de urnas com mais concentração de eleitores
Como será que isso pode refletir nos resultados?
Vamos analisar isso visualizando os resultados por UF e por tipo de urna
votosPresidente[['UF','Lula','Bolsonaro']].groupby(['UF']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos Geral")
votosPresidente.query("modelo == 'UE2020'")[['UF','Lula','Bolsonaro']].groupby(['UF']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos UE2020")
votosPresidente.query("modelo != 'UE2020'")[['UF','Lula','Bolsonaro']].groupby(['UF']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos Não UE2020")
Text(0.5, 1.0, 'Votos Não UE2020')
votosPresidente[['UF','modelo','Lula','Bolsonaro']].groupby(['modelo']).sum(numeric_only=True).plot.bar(color=['red','blue']).set_title("Votos por Tipo de Urna")
Text(0.5, 1.0, 'Votos por Tipo de Urna')
Tecnicos universitários disseram que as distorções nos gráficos de disperção são devido às urnas UE2020 terem maior quantidade de eleitores.
Extraímos as quantidades de eleitores que compareceram por tipo de urna
comparecimento = urnas[['modelo','qtdComparecimento']].query("modelo != 'SemLog'")
comparecimentoGroup = comparecimento.groupby(['modelo']).mean().join(
comparecimento.groupby(['modelo']).max(), lsuffix='_mean').join(
comparecimento.groupby(['modelo']).min(), lsuffix='_max').rename(
{"qtdComparecimento_mean":"média", "qtdComparecimento_max":"max","qtdComparecimento":"min"}, axis=1)
comparecimentoGroup
| média | max | min | |
|---|---|---|---|
| modelo | |||
| UE2009 | 252.951269 | 605 | 1 |
| UE2010 | 250.712626 | 582 | 7 |
| UE2011 | 248.430713 | 485 | 9 |
| UE2013 | 248.399319 | 437 | 13 |
| UE2015 | 255.211165 | 588 | 8 |
| UE2020 | 279.468146 | 508 | 7 |
ax = comparecimentoGroup.groupby(['modelo']).sum(numeric_only=True).plot.bar(color=['blue','red','green'], figsize=(8,6))
ax.set_title("Comparecimento por Tipo de Urna")
Text(0.5, 1.0, 'Comparecimento por Tipo de Urna')
Vamos repetir os mesmos gráficos para verificar a afirmação de que os gráficos de dispersão não são válidos para comparação pois a distorção (trava de voto) seria causada pela quantidade de votos por urna. Assim, vamos limitar os gráficos abaixo para as urnas com 240 a 260 votos (entre a média das urnas 2010, 2015 e 2020)
votosPresidenteRestrito = votosPresidente.query("modelo == 'UE2020' and Votos > 240 and Votos < 260")
data = votosPresidenteRestrito
plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
plt.title("Urnas UE2020 - Brasil")
plt.xlabel("Bolsonaro")
plt.ylabel("Lula")
plt.show()
C:\Users\coelh\AppData\Local\Temp\ipykernel_26828\1050429937.py:2: UserWarning: No data for colormapping provided via 'c'. Parameters 'vmax' will be ignored plt.scatter(data['Bolsonaro'],data['Lula'], s=1, vmax=200)
Olhando o gráfico acima fica então evidente de que a afirmação dos especialistas está correto, e os gráficos de dispersão não comprovam que houve fraude.